﻿using System;
using System.Collections.Concurrent;
using System.Concurrency;
using System.IO;
using System.Linq;
using System.Threading;
using Vidyano;

namespace RapidDownloader.Model
{
    public sealed class AsyncWriter<TFile> : ViewModelBase
        where TFile : class, IEquatable<TFile>
    {
        #region Fields

        private readonly Func<TFile, string> getFileName;
        private readonly IScheduler scheduler;
        private IDisposable writerCompleted;
        private readonly ConcurrentDictionary<TFile, Writer> writers = new ConcurrentDictionary<TFile, Writer>();

        private long _MaxBufferSize;

        #endregion

        #region Constructors

        public AsyncWriter(Func<TFile, string> getFileName, string name = "AsyncWriter")
        {
            if (getFileName == null)
                throw new ArgumentNullException("getFileName");
            this.getFileName = getFileName;

            scheduler = new EventLoopScheduler(name);
            writerCompleted = MessageBus.Listen<FileWriteProgressMessage<TFile>>()
                .Where(fpm => writers.ContainsKey(fpm.File))
                .Subscribe(fpm =>
                               {
                                   if (fpm.IsCompleted)
                                   {
                                       Writer writer;
                                       if (writers.TryRemove(fpm.File, out writer))
                                           writer.Dispose();
                                   }
                                   BufferChanged();
                               });

            MaxBufferSize = 256 * 1024 * 1024;
        }

        #endregion

        #region Properties

        public TFile[] Files
        {
            get { return writers.Keys.ToArray(); }
        }

        public bool IsDisposing { get; private set; }

        public bool IsDisposed { get; private set; }

        public long MaxBufferSize
        {
            get { return _MaxBufferSize; }
            set
            {
                if (_MaxBufferSize != value)
                {
                    _MaxBufferSize = value;
                    SendPropertyChanged("MaxBufferSize");

                    BufferChanged();
                }
            }
        }

        public long UsedBufferSize
        {
            get { return writers.Values.Sum(w => w.UsedBufferSize); }
        }

        #endregion

        #region Public Methods

        public bool Add(TFile file, byte[] data)
        {
            if (file == null)
                throw new ArgumentNullException("file");
            if (string.IsNullOrWhiteSpace(getFileName(file)))
                throw new ArgumentNullException("file");
            if (data == null)
                throw new ArgumentNullException("data");

            if (!IsDisposing)
            {
                Writer writer;
                if (!writers.TryGetValue(file, out writer))
                    throw new InvalidOperationException("File was not found in AsyncWriter.\r\nUse CreateFile to add the file first.");

                writer.Add(data);
                scheduler.Schedule(DoWork);

                BufferChanged();

                while (writer.HasData && UsedBufferSize > MaxBufferSize)
                {
                    if (IsDisposing)
                        return false;
                    Thread.Sleep(10);
                }
                return true;
            }
            return false;
        }

        public bool CreateFile(TFile file, bool append, long totalSize)
        {
            if (file == null)
                throw new ArgumentNullException("file");
            var fileName = getFileName(file);
            if (string.IsNullOrWhiteSpace(fileName))
                throw new ArgumentNullException("file");
            if (totalSize <= 0)
                throw new ArgumentOutOfRangeException("totalSize", totalSize, "Value should be positive.");

            if (IsDisposing)
                return false;

            var created = false;
            writers.GetOrAdd(file, f =>
                                       {
                                           created = true;
                                           return new Writer(f, fileName, append, totalSize);
                                       });
            return created;
        }

        public override void Dispose()
        {
            if (IsDisposing)
                return;

            IsDisposing = true;
            Action baseDispose = base.Dispose;
            scheduler.Schedule(() =>
                                   {
                                       if (writerCompleted != null)
                                       {
                                           writerCompleted.Dispose();
                                           writerCompleted = null;
                                       }

                                       writers.Values.Run(w => w.Dispose());
                                       writers.Clear();

                                       baseDispose();

                                       IsDisposed = true;
                                   });
        }

        public void FileCompleted(TFile file)
        {
            Writer fileWriter;
            if (writers.TryGetValue(file, out fileWriter))
                fileWriter.NeedsClosing = true;

            scheduler.Schedule(() =>
                                   {
                                       if (!IsDisposed)
                                       {
                                           Writer writer;
                                           if (writers.TryRemove(file, out writer))
                                               writer.Dispose();
                                       }
                                   });
        }

        public override string ToString()
        {
            return string.Format("Writer {0} files / {1} bytes remaining", writers.Count, UsedBufferSize);
        }

        #endregion

        #region Private Methods

        private void BufferChanged()
        {
            MessageBus.Send(new BufferChangedMessage(MaxBufferSize, UsedBufferSize));
        }

        private void DoWork()
        {
            var writer = writers.Values.FirstOrDefault(w => w.NeedsClosing) ?? writers.Values.OrderByDescending(w => w.UsedBufferSize).FirstOrDefault(w => w.HasData);
            if (writer != null)
                writer.WriteData();
        }

        #endregion

        #region Nested Types

        private sealed class Writer : IDisposable
        {
            #region Fields

            private readonly ConcurrentQueue<byte[]> bytes = new ConcurrentQueue<byte[]>();
            private TFile file;
            private readonly long totalOrRemainingSize;

            private long bytesWritten;
            private Stream stream;

            private const int fileBuffer = 1024 * 1024;

            #endregion

            #region Constructors

            public Writer(TFile file, string fileName, bool append, long totalSize)
            {
                this.file = file;
                if (append && File.Exists(fileName))
                    totalOrRemainingSize = totalSize - new FileInfo(fileName).Length;
                else
                    totalOrRemainingSize = totalSize;

                stream = new FileStream(fileName, append ? FileMode.Append : FileMode.Create, FileAccess.Write, FileShare.Read, fileBuffer, FileOptions.SequentialScan);
                stream.SetLength(totalSize);
            }

            #endregion

            #region Properties

            public long UsedBufferSize
            {
                get { return bytes.Sum(b => b.LongLength); }
            }

            public bool HasData { get; private set; }

            public bool NeedsClosing { get; set; }

            #endregion

            #region Public Methods

            public void Add(byte[] data)
            {
                if (stream == null)
                    throw new ObjectDisposedException("stream");

                bytes.Enqueue(data);
                HasData = true;
            }

            public void Dispose()
            {
                if (HasData)
                    WriteData();

                HasData = false;
                if (stream != null)
                {
                    byte[] data;
                    while (bytes.TryDequeue(out data)) { }

                    stream.SetLength(stream.Position);
                    stream.Flush();
                    stream.Close();
                    stream = null;

                    if (file != null)
                    {
                        MessageBus.Send(new FileWriteProgressMessage<TFile>(file, bytesWritten, bytesWritten));
                        file = null;
                    }
                }
            }

            public override string ToString()
            {
                return string.Format("Writer: {0} - ({1})", file, UsedBufferSize);
            }

            public void WriteData()
            {
                byte[] data;
                while (bytes.TryDequeue(out data))
                {
                    if (stream == null)
                        return;

                    stream.Write(data, 0, data.Length);
                    bytesWritten += data.Length;
                    if (totalOrRemainingSize > 0 && bytesWritten >= totalOrRemainingSize)
                    {
                        HasData = false;
                        Dispose();
                        return;
                    }
                    MessageBus.Send(new FileWriteProgressMessage<TFile>(file, bytesWritten, totalOrRemainingSize));
                }
                HasData = false;
            }

            #endregion
        }

        #endregion
    }
}